On this page

Skip to content

A Brief Discussion on Exception Handling and State Restoration for SaveChanges() in Entity Framework

TLDR

  • After SaveChanges() fails, ChangeTracker retains all change states, causing subsequent operations to continue failing.
  • It is recommended to override the SaveChanges() method in DbContext and manually restore the Entity state via ChangeTracker when catching DbUpdateException.
  • For Entities in the Added state, set them to Detached; for the Modified state, restore CurrentValues to OriginalValues and set them to Unchanged.
  • If the Entity structure involves Foreign Keys or complex navigation properties, restoring EntityState may lead to inconsistencies between the cache and the database; this restoration mechanism is not recommended for complex relationship scenarios.
  • Distinguish whether error messages should be visible to the frontend; choose to record detailed errors in logs or re-throw them via custom Exceptions.

Exception Handling in Entity Framework

During development, handling exceptions thrown by SaveChanges() is key to ensuring system stability. Common exception types include DbUpdateException (save failure) and DbUpdateConcurrencyException (concurrency conflict).

Recommendations for Error Message Handling

When to encounter this issue: When the system throws underlying database errors, and the developer needs to balance log detail with frontend security.

  • When the original error is visible to the frontend: Extract the full error information from the InnerException when writing to logs, and hide the details from the frontend.
  • When the error is not visible to the frontend: It is recommended to override SaveChanges() in DbContext, catch the exception, and re-throw an Exception containing the full error message to facilitate clear separation of responsibilities.

State Restoration When SaveChanges() Fails

When to encounter this issue: After SaveChanges() fails, ChangeTracker still retains the failed change state, causing subsequent normal write operations to include the failed data, leading to a chain reaction of failures.

If you wish to ignore the changes when an error occurs, you can restore the ChangeTracker state using the following implementation:

csharp
private static void ResetEntityState(EntityEntry entry) {
    switch (entry.State) {
        case EntityState.Added:
            entry.State = EntityState.Detached;
            break;
        case EntityState.Modified:
            entry.CurrentValues.SetValues(entry.OriginalValues);
            entry.State = EntityState.Unchanged;
            break;
        case EntityState.Deleted:
            // For related data, it is recommended to set to Detached to avoid navigation property synchronization anomalies
            entry.State = entry.Entity is Dictionary<string, object>
                ? EntityState.Detached
                : EntityState.Unchanged;
            break;
    }
}

WARNING

The method of restoring Entity State after a SaveChanges() failure is only applicable to simple Entity structures without foreign keys. If complex navigation properties are involved, restoring the state may lead to inconsistencies between the DbContext cache and the database content.

Test Results and Limitations

When to encounter this issue: When there are foreign key relationships between Entities, and Add() or Remove() operations are performed via navigation properties.

Experimental results show that when attempting to restore related data in the EntityState.Deleted state, setting the state to Unchanged causes navigation properties to fail to reload correctly from the database (because the DbContext has already cached the object). If set to Detached, although navigation properties can be restored, there are still risks in overall state management.

WARNING

Using DbSet.Add() to add an Entity with the same PK as already queried data will throw an InvalidOperationException. Since this exception occurs during the Add() phase rather than the SaveChanges() phase, the error handling mechanism mentioned above cannot intercept this type of error.

Change Log

  • 2024-08-17 Initial version created.